﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Diagnostics.Contracts;
using System.Reflection;

using ReactiveLang;
using ReactiveLang.State;

using E = System.Linq.Expressions.Expression;

namespace ReactiveLang.ExpressionConverter
{
    using ReactiveLang.Util;
    using Util;

    class Converter : ExpressionVisitor<E>
    {
        public E Convert(E expression)
        {
            Contract.Requires(expression != null);

            return Visit(expression);
        }
        public Expression<Func<TResult>> Convert<TResult>(E expression)
        {
            Contract.Requires(expression != null);
            var result = Convert(expression);

            return E.Lambda<Func<TResult>>(result);
        }
        public Func<TResult> ConvertEval<TResult>(E expression)
        {
            var lambda = Convert<TResult>(expression);
            return lambda.Compile();
        }

        internal IEnumerable<E> Visit(IEnumerable<E> expressions)
        {
            if (expressions == null)
                return Enumerable.Empty<E>();
            return expressions.Select(Visit);
        }

        Dictionary<E, E> _cache = new Dictionary<E, E>();
        protected override E Visit(E expr)
        {
            Contract.Ensures(Contract.Result<E>() == null || Contract.Result<E>().Type == typeof(IExpression<>).MakeGenericType(expr.Type));

            if (expr == null)
                return null;

            switch (expr.NodeType)
            {
                case ExpressionType.AddAssign:
                case ExpressionType.AddAssignChecked:
                case ExpressionType.AndAssign:
                case ExpressionType.Assign:
                case ExpressionType.Block:
                case ExpressionType.DebugInfo:
                case ExpressionType.DivideAssign:
                case ExpressionType.Dynamic: // todo
                case ExpressionType.ExclusiveOrAssign:
                case ExpressionType.Extension:
                case ExpressionType.Goto:
                case ExpressionType.Increment:
                case ExpressionType.Label:
                case ExpressionType.LeftShiftAssign:
                case ExpressionType.Loop:
                case ExpressionType.ModuloAssign:
                case ExpressionType.MultiplyAssign:
                case ExpressionType.MultiplyAssignChecked:
                case ExpressionType.OnesComplement:
                case ExpressionType.OrAssign:
                case ExpressionType.PostDecrementAssign:
                case ExpressionType.PostIncrementAssign:
                case ExpressionType.PowerAssign:
                case ExpressionType.PreDecrementAssign:
                case ExpressionType.PreIncrementAssign:
                case ExpressionType.RightShiftAssign:
                case ExpressionType.RuntimeVariables:
                case ExpressionType.SubtractAssign:
                case ExpressionType.SubtractAssignChecked:
                    throw new NotSupportedException(string.Format("E type {0} is not supported: {1}", expr.NodeType, expr));
            }

            E result;
            if (!_cache.TryGetValue(expr, out result))
                _cache[expr] = result = base.Visit(expr);

            return base.Visit(expr);
        }

        #region Type members

        E ConvertMember(E expression)
        {
            MemberInfo member;
            E[] memberArgs =  expression.ExtractMemberArgs(out member).ToArray();
            Contract.Assume(member != null);

            if (member.Name == "Value" && member.DeclaringType.IsOfGenericType(typeof(Variable<>)))
                return E.Convert(memberArgs.Single(), typeof(IExpression<>).MakeGenericType(expression.Type));

            var p = E.Parameter(typeof(IList<object>), "p");
            var pItems = from i in Enumerable.Range(0, memberArgs.Length)
                         select E.Property(p, "Item", E.Constant(i))
                            .ExpectType(memberArgs[i].Type);
            var valueSelector = E.Lambda(
                expression.Replace(
                    memberArgs,
                    pItems.ToArray()
                    ),
                p
                );

            var arguments = E.NewArrayInit(typeof(IExpression<object>),
                from a in memberArgs
                select Visit(a).Box());

            E options = _nullOptions; 
            if (_interval.Any())
            {
                options = E.MemberInit(
                    E.New(typeof(ChangeNotifierOptions)),
                    E.Bind(typeof(ChangeNotifierOptions).GetProperty("Interval"), _interval.Peek())
                    );
            }

            return E.Call(typeof(Expression), "Member", new [] { expression.Type }, 
                E.Constant(member), 
                arguments,
                valueSelector, 
                options);
        }

        protected override E VisitMember(MemberExpression exp)
        {
            return ConvertMember(exp);
        }
        protected override E VisitIndex(IndexExpression exp)
        {
            return ConvertMember(exp);
        }

        static readonly E _nullOptions= E.Constant(null, typeof(ChangeNotifierOptions));
        Stack<E> _interval = new Stack<E>();
        protected override E VisitMethodCall(MethodCallExpression exp)
        {
            if (exp.Method.ReturnType != typeof(void))
            {
                if (exp.Method.DeclaringType == typeof(ReactiveOptions) && exp.Method.Name == "Interval")
                {
                    Contract.Assume(exp.Arguments.Count == 2);
                    _interval.Push(Visit(exp.Arguments[1]));
                    try
                    {
                        return Visit(exp.Arguments[0]);
                    }
                    finally
                    {
                        _interval.Pop();
                    }
                }

                return ConvertMember(exp);
            }

            return base.VisitMethodCall(exp);
        }

        #endregion

        protected override E VisitInvocation(InvocationExpression exp)
        {
            var allSources = new List<E> { Visit(exp.Expression) };
            allSources.AddRange(Visit(exp.Arguments));

            return allSources.Binary(x => E.Invoke(x[0], x.Skip(1)));
        }
        protected override E VisitConstant(ConstantExpression exp)
        {
            return Extensions.Constant(exp);
        }
        protected override E VisitBinary(BinaryExpression exp)
        {
            var left = Visit(exp.Left);
            var right = Visit(exp.Right);

            var x = E.Parameter(exp.Left.Type);
            var y = E.Parameter(exp.Right.Type);
            var selector = E.Lambda(E.MakeBinary(exp.NodeType, x, y), x, y);

            return left.Binary(right, selector);
        }
        protected override E VisitConditional(ConditionalExpression exp)
        {
            return Visit(exp.Test).If(Visit(exp.IfTrue), Visit(exp.IfFalse));
        }
        protected override E VisitUnary(UnaryExpression exp)
        {
            var operand = Visit(exp.Operand);

            var x = E.Parameter(exp.Operand.Type);
            var selector = E.Lambda(E.MakeUnary(exp.NodeType, x, exp.Type), x);

            return operand.Unary(selector);
        }
        protected override E VisitDefault(DefaultExpression exp)
        {
            return Extensions.Constant(exp);
        }

        protected override E VisitLambda(LambdaExpression exp)
        {
            var newBody = E.Parameter(exp.Body.Type);
            var createLambda = E.Lambda(
                E.Lambda(newBody, exp.Parameters),
                newBody);

            return Visit(exp.Body).Unary(createLambda);
        }

        protected override E VisitNew(NewExpression nex)
        {
            return Visit(nex.Arguments).ToList()
                .Binary(args => E.New(nex.Constructor, args));
        }

        #region Member init
        protected override E VisitMemberInit(MemberInitExpression exp)
        {
            throw new NotImplementedException();
            //return VisitNew(exp.NewExpression).CombineLatest(Visit(exp.Bindings), (obj, bindings) =>
            //{
            //    foreach (var b in bindings)
            //        b(obj);
            //    return obj;
            //});
        }
        #endregion

        protected override E VisitNewArray(NewArrayExpression exp)
        {
            var expressions = Visit(exp.Expressions).ToList();
            Type elementType = exp.Type.GetElementType();

            switch (exp.NodeType)
            {
                case ExpressionType.NewArrayBounds:
                    return expressions.Binary(e => E.NewArrayBounds(elementType, e));

                case ExpressionType.NewArrayInit:
                    return expressions.Binary(e => E.NewArrayInit(elementType, e));

                default:
                    throw new ArgumentException("exp");
            }
        }
        protected override E VisitTypeBinary(TypeBinaryExpression exp)
        {
            var x = E.Parameter(exp.Expression.Type);
            E selectorBody;
            if (exp.NodeType == ExpressionType.TypeIs)
                selectorBody = E.TypeIs(x, exp.TypeOperand);
            else if (exp.NodeType == ExpressionType.TypeAs)
                selectorBody = E.TypeAs(x, exp.TypeOperand);
            else
                throw new ArgumentException("exp");

            return Visit(exp.Expression).Unary(E.Lambda(selectorBody, x));
        }
    }
}
